package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.UserDTO;
import com.hmdp.dto.UserRequest;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.ILock;
import com.hmdp.utils.MyLock;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBloomFilter;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RBloomFilter<String> bloomFilter;

    public static final String SALT = "vvv123";


    @Override
    public void sendCode(String phone) {
        // 1. 检查手机号是否正确
        if (StringUtils.isEmpty(phone)) {
            throw new RuntimeException("手机号错误");
        }
        // 2. 判断一下是否已经发送了验证码
        // 这里会出现并发问题，100个线程到这一行代码，发现都没有code
        String key = RedisConstants.LOGIN_CODE_KEY + phone;
        Boolean hasCode = stringRedisTemplate.hasKey(key);
        if (hasCode) {
            throw new RuntimeException("请不要重复发送验证码");
        }
        // 3. 获取互斥锁，保证只有一个线程能去发送验证码
        ILock lock = new MyLock(RedisConstants.LOCK_CODE + phone, stringRedisTemplate);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            throw new RuntimeException("请不要重复发送验证码");
        }
        try {
            // 4. 生产验证码
            String code = RandomUtil.randomNumbers(6);
            // 5. 把验证码保存到redis中
            stringRedisTemplate.opsForValue().set(key, code, 60, TimeUnit.SECONDS);
            // 6. 发送验证码
            log.debug("验证码是：{}", code);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public String login(LoginFormDTO loginForm) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        // 1. 参数检查
        if (StringUtils.isAnyEmpty(phone, code)) {
            throw new RuntimeException("手机号或者验证码为空");
        }
//        // 1.1 检查手机号是否正确
//        if (RegexUtils.isPhoneInvalid(phone)) {
//            throw new RuntimeException("手机号错误");
//        }
        // 2. 校验验证码
        String key = RedisConstants.LOGIN_CODE_KEY + phone;
        String cacheCode = stringRedisTemplate.opsForValue().get(key);
        if (cacheCode == null || !code.equals(cacheCode)) {
            throw new RuntimeException("验证码错误");
        }
        // 2.1 验证码正确，删除验证码
        stringRedisTemplate.delete(key);
        // 3. 根据手机号查询用户
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, phone);
        User dbUser = getOne(queryWrapper);
        // 4. 如果不存在创建用户
        if (dbUser == null) {
            dbUser = createUserWithPhone(phone);
        }
        // 5. 生产token
        String token = UUID.randomUUID().toString(true);

        // 6. 将用户保存到redis中
        UserDTO userDTO = BeanUtil.copyProperties(dbUser, UserDTO.class);
        // 因为这里用的是stringRedisTemplate 所以所以的key和value都需要是string类型,但是这里的UserId是Long类型，所以我们需要转一下
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>()
                ,CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldKey, fieldValue) -> fieldValue.toString()));
        // 6.1 保存到redis中
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES);
        // 7. 返回token
        return token;
    }

    @Override
    public void logout(String token) {
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        // 2. 把redis中的用户状态给删除
        stringRedisTemplate.delete(tokenKey);
    }

    @Override
    public void signUp() {
        // 1. 获取当前用户的userId
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("请登录");
        }
        Long userId = user.getId();
        // 2. 获取当前日期
        LocalDateTime now = LocalDateTime.now();
        String dayKeySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy:MM"));
        int offset = now.getDayOfMonth() - 1;
        // 3. 判断今天是否已经签到
        String signKey = RedisConstants.USER_SIGN_KEY + userId + dayKeySuffix;
        Boolean isSignUp = stringRedisTemplate.opsForValue().getBit(signKey, offset);
        if (BooleanUtil.isTrue(isSignUp)) {
            // 3.1 已经签到直接报错
            throw new RuntimeException("不能重复签到");
        }
        // 3.2 未签到让bit = 1
        stringRedisTemplate.opsForValue().setBit(signKey, offset, true);
    }

    @Override
    public int queryUserSignOfCount() {
        // 1. 获取当前用户的userId
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            throw new RuntimeException("请登录");
        }
        Long userId = user.getId();
        // 2. 获取当前日期
        LocalDateTime now = LocalDateTime.now();
        String dayKeySuffix = now.format(DateTimeFormatter.ofPattern(":yyyy:MM"));
        int offset = now.getDayOfMonth() - 1;
        String signKey = RedisConstants.USER_SIGN_KEY + userId + dayKeySuffix;
        // 3. 从redis中获取bitMap
        List<Long> list = stringRedisTemplate.opsForValue().bitField(
                signKey,
                BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(offset)).valueAt(0)
        );
        if (CollectionUtils.isEmpty(list)) {
            return 0;
        }
        // 4. 遍历获得连续签到的次数
        Long num = list.get(0);
        int signOfCount = 0;
        while ((num & 1) == 0) {
            signOfCount++;
            num >>>= 1;
        }
        // 5. 返回
        return signOfCount;
    }

    @Override
    public long queryMonthlyUVStatistics() {
        LocalDateTime now = LocalDateTime.now();
        String timeSuffix = now.format(DateTimeFormatter.ofPattern("yyyy:MM"));
        String key = RedisConstants.UV_STATISTICS + timeSuffix;
        Long size = stringRedisTemplate.opsForHyperLogLog().size(key);
        return size;
    }


    @Override
    public void register(UserRequest userRequest) {
        String nickName = userRequest.getNickName();
        String password = userRequest.getPassword();
        if (StringUtils.isAnyBlank(nickName, password)) {
            throw new UnsupportedOperationException("nickName or password can not be empty !");
        }
        // 1. 使用布隆过滤器检查用户名是否存在
        if (bloomFilter.contains(nickName)) {
            throw new UnsupportedOperationException("User already registered");
        }
        // 1.1 布隆过滤器的误判操作
        Integer count = lambdaQuery().eq(User::getNickName, nickName).count();
        if (count > 0) {
            throw new UnsupportedOperationException("User already registered");
        }
        // TODO 密码要加盐加密 + 分布式锁
        ILock lock = new MyLock(RedisConstants.LOCK_REGISTER + nickName, stringRedisTemplate);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            throw new UnsupportedOperationException("Cannot duplicate registration! ");
        }
        try {
            // 2. 用户名不存在则创建对象，并写入数据库
            createUserWithNickNameAndPassword(nickName, password);
            // 3. 将用户名存储到布隆过滤器中
            bloomFilter.add(nickName);
        } finally {
            lock.unlock();
        }
    }

    private User createUserWithNickNameAndPassword(String nickName, String password) {
        User user = new User();
        user.setNickName(nickName);
        // 密码加密
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + password).getBytes());
        user.setPassword(encryptPassword);
        // 3. 写入数据库
        save(user);
        return user;
    }


    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //baseMapper.insert(user);
        //mp service中保存的方法
        save(user);
        return user;
    }

}
